<?php

/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Symfony\UX\Translator;

use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Translation\MessageCatalogueInterface;
use Symfony\UX\Translator\MessageParameters\Extractor\IntlMessageParametersExtractor;
use Symfony\UX\Translator\MessageParameters\Extractor\MessageParametersExtractor;
use Symfony\UX\Translator\MessageParameters\Printer\TypeScriptMessageParametersPrinter;

/**
 * @author Hugo Alliaume <hugo@alliau.me>
 *
 * @final
 *
 * @experimental
 *
 * @phpstan-type Domain string
 * @phpstan-type Locale string
 * @phpstan-type MessageId string
 */
class TranslationsDumper
{
    private array $excludedDomains = [];
    private array $includedDomains = [];

    public function __construct(
        private string $dumpDir,
        private bool $dumpTypeScript,
        private MessageParametersExtractor $messageParametersExtractor,
        private IntlMessageParametersExtractor $intlMessageParametersExtractor,
        private TypeScriptMessageParametersPrinter $typeScriptMessageParametersPrinter,
        private Filesystem $filesystem,
    ) {
    }

    public function dump(MessageCatalogueInterface ...$catalogues): void
    {
        $this->filesystem->mkdir($this->dumpDir);
        $this->filesystem->remove($fileIndexJs = $this->dumpDir.'/index.js');
        $this->filesystem->remove($fileIndexDts = $this->dumpDir.'/index.d.ts');

        $this->filesystem->appendToFile(
            $fileIndexJs,
            \sprintf(<<<'JS'
                // This file is auto-generated by the Symfony UX Translator. Do not edit it manually.

                export const localeFallbacks = %s;
                export const messages = {

                JS,
                json_encode($this->getLocaleFallbacks(...$catalogues), \JSON_THROW_ON_ERROR)
            ));

        if ($this->dumpTypeScript) {
            $this->filesystem->appendToFile(
                $fileIndexDts,
                <<<'TS'
                    // This file is auto-generated by the Symfony UX Translator. Do not edit it manually.
                    import { Message, NoParametersType, LocaleType } from '@symfony/ux-translator';

                    export declare const localeFallbacks: Record<LocaleType, LocaleType>;
                    export declare const messages: {

                    TS
            );
        }

        foreach ($this->getTranslations(...$catalogues) as $translationId => $translationsByDomainAndLocale) {
            $translationId = str_replace('"', '\\"', $translationId);
            $this->filesystem->appendToFile($fileIndexJs, \sprintf(
                '    "%s": %s,%s',
                $translationId,
                json_encode(['translations' => $translationsByDomainAndLocale], \JSON_THROW_ON_ERROR),
                "\n"
            ));

            if ($this->dumpTypeScript) {
                $this->filesystem->appendToFile($fileIndexDts, \sprintf(
                    '    "%s": %s;%s',
                    $translationId,
                    $this->getTranslationsTypeScriptTypeDefinition($translationsByDomainAndLocale),
                    "\n"
                ));
            }
        }

        $this->filesystem->appendToFile($fileIndexJs, '};'."\n");

        if ($this->dumpTypeScript) {
            $this->filesystem->appendToFile($fileIndexDts, '};'."\n");
        }
    }

    public function addExcludedDomain(string $domain): void
    {
        if ($this->includedDomains) {
            throw new \LogicException('You cannot set both "excluded_domains" and "included_domains" at the same time.');
        }
        $this->excludedDomains[] = $domain;
    }

    public function addIncludedDomain(string $domain): void
    {
        if ($this->excludedDomains) {
            throw new \LogicException('You cannot set both "excluded_domains" and "included_domains" at the same time.');
        }
        $this->includedDomains[] = $domain;
    }

    /**
     * @return array<MessageId, array<Domain, array<Locale, string>>>
     */
    private function getTranslations(MessageCatalogueInterface ...$catalogues): array
    {
        $translations = [];

        foreach ($catalogues as $catalogue) {
            $locale = $catalogue->getLocale();
            foreach ($catalogue->getDomains() as $domain) {
                if (\in_array($domain, $this->excludedDomains, true)) {
                    continue;
                }
                if ($this->includedDomains && !\in_array($domain, $this->includedDomains, true)) {
                    continue;
                }
                foreach ($catalogue->all($domain) as $id => $message) {
                    $realDomain = $catalogue->has($id, $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX)
                        ? $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX
                        : $domain;

                    $translations[$id] ??= [];
                    $translations[$id][$realDomain] ??= [];
                    $translations[$id][$realDomain][$locale] = $message;
                }
            }
        }

        return $translations;
    }

    /**
     * @param array<Domain, array<Locale, string>> $translationsByDomainAndLocale
     *
     * @throws \Exception
     */
    private function getTranslationsTypeScriptTypeDefinition(array $translationsByDomainAndLocale): string
    {
        $parametersTypes = [];
        $locales = [];

        foreach ($translationsByDomainAndLocale as $domain => $translationsByLocale) {
            foreach ($translationsByLocale as $locale => $translation) {
                try {
                    $parameters = str_ends_with($domain, MessageCatalogueInterface::INTL_DOMAIN_SUFFIX)
                        ? $this->intlMessageParametersExtractor->extract($translation)
                        : $this->messageParametersExtractor->extract($translation);
                } catch (\Throwable $e) {
                    throw new \Exception(\sprintf('Error while extracting parameters from message "%s" in domain "%s" and locale "%s".', $translation, $domain, $locale), previous: $e);
                }

                $parametersTypes[$domain] = $this->typeScriptMessageParametersPrinter->print($parameters);
                $locales[] = $locale;
            }
        }

        $typeScriptParametersType = [];
        foreach ($parametersTypes as $domain => $parametersType) {
            $typeScriptParametersType[] = \sprintf("'%s': { parameters: %s }", $domain, $parametersType);
        }

        return \sprintf(
            'Message<{ %s }, %s>',
            implode(', ', $typeScriptParametersType),
            implode('|', array_map(fn (string $locale) => "'$locale'", array_unique($locales))),
        );
    }

    private function getLocaleFallbacks(MessageCatalogueInterface ...$catalogues): array
    {
        $localesFallbacks = [];

        foreach ($catalogues as $catalogue) {
            $localesFallbacks[$catalogue->getLocale()] = $catalogue->getFallbackCatalogue()?->getLocale();
        }

        return $localesFallbacks;
    }
}
